package Phase2Tests;

//import java.net.InetAddress;
//import java.net.UnknownHostException;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;
import java.util.TreeSet;

import Phase1.Server;
import Phase1.ObjectDB;
import Phase2.Node;

import org.junit.Assert;
import org.junit.Test;

import plugin.BroadcastWithAck;
import plugin.KeyValuePair;
import plugin.Visitor;

public class DistributedTest 
{
	
	private static final int SLEEP_1000 = 1000;
	private static final int MAX_WEB_SIZE = 6;
	private int PORT;
	private Node root;
	private ObjectDB db;
	private Random BIG_RANDOM;
	
	public void setUp()
	{
		
	}
	
	@Test
	public void distributedTest()
	{
		BIG_RANDOM = new Random(System.currentTimeMillis());

		try 
		{
			db = ObjectDB.getDatabase();
			db.clear();
			Scanner scan = new Scanner(System.in);
			PORT = getPort(scan);
			Server server = new Server(PORT);
			
			root = new Node(PORT);
			db.put(root.getGlobalID(), root);
			
			Thread.sleep(SLEEP_1000);
			
			connectToApp(root, scan);
			
			Boolean loop = true;
			
			while (loop)
			{
				DistributedTest.print("Enter a number from 0-10, or -1 to end"
						+ "\nEnter 0 to print the HyPeerWeb"
						+ "\nEnter 1 to insert a new Node in the HyPeerWeb"
						+ "\nEnter 2 to delete a node"
						+ "\nEnter 3 to print the app's database" 
						+ "\nEnter 4 to send a message"
						+ "\nEnter 5 to run random message script"
						+ "\nEnter 6 to broadcast a message"
						+ "\nEnter 9 to start the add/delete program"
						+ "\nEnter 27 to reconnect to an app");

				int input = -2;
				try
				{
					input = scan.nextInt();
				}
				catch (java.util.InputMismatchException e)
				{
					Thread.sleep(0);
				}
				
				switch(input)
				{
					// print the HyPeerWeb
					case 0: 
						DistributedTest.print("Current HyPeerWeb");
						DistributedTest.print(root.printAllNode() + "\n");
						break;
					// insert a node
					case 1: 
						insertNode();
						break;
					// delete a node
					case 2:
						deleteNode();
						break;
					// print the database
					case 3: 
						print("Printing out the database\n");
						print(this.db.toString());
						print("\ndone\n");
						break;
					// send message from one node to another
					case 4:
						sendMessagePrompt(scan);
						break;
					// send 1000 random messages
					case 5:
						for (int i = 0; i < 100; i++)
						{
							sendRandomMessage();
						}						
						break;
					// broadcast something
					case 6:
						broadcastPrompt(scan);
						break;
					// calls the add/delete program
					case 9:
						runProgram();
						break;
					// end
					case -1:
						loop = false;
						break;
					case 27:
						if (getNode() != null)
						{
							DistributedTest.print("Can only reconnect if no nodes present!");
						}
						else
						{
							root = new Node(PORT);
							db.put(root.getGlobalID(), root);
							Thread.sleep(SLEEP_1000);
							connectToApp(root, scan);
						}
						break;
					default:
						DistributedTest.print("Try again");
				}
			}
			server.done();
		
			Thread.sleep(SLEEP_1000);
			DistributedTest.print("Done!");
		
		} 
		catch (InterruptedException e) 
		{
			e.printStackTrace();
		} 
		catch (Exception e)
		{
			e.printStackTrace();
			Assert.assertTrue(e == null);
		}
		
		
	}
	
	private void randomBroadcast() throws InterruptedException
	{

		Node node = getNode();
		
		String message = "Hello, broadcasting from node " + node.getId(false);
		
		broadcast(node, message);		
	}
	
	private void broadcastPrompt(Scanner scan) throws InterruptedException
	{
		DistributedTest.print("What node would you like to send a broadcast from?");
		int source = scan.nextInt();
		Node sourceNode = db.get(source);
		if (sourceNode == null)
		{
			DistributedTest.print("Unable to find source node!");
			return;
		}
		
		DistributedTest.print("What message would you like to broadcast?");
		String message = scan.next();
		message += scan.nextLine();
		
		broadcast(sourceNode, message);
	}
	
	private void broadcast(Node sourceNode, String message) throws InterruptedException 
	{
		Visitor broadcast = new BroadcastWithAck();
		TreeSet<Object> t = new TreeSet<Object>();
		t.addAll((ArrayList<Object>) sourceNode.acceptWithResponse(broadcast, message));
		String response = "";
		Node tempNode;
		for (Object obj : t)
		{
			tempNode = (Node) obj;
			response += tempNode.getId(false) + " ";
		}
		DistributedTest.print("Node " + sourceNode.getId(false) + " received response from nodes: \n" + response);
		
	}

	private void sendRandomMessage() throws InterruptedException
	{
		Node sourceNode = this.getNode();
		DistributedTest.print("Approx web size: " + (int) Math.pow(2, sourceNode.getHeight(false)));
		int destination = this.randomIndex((int) Math.pow(2, sourceNode.getHeight(false)));
		String message = "Random message #" + this.randomIndex(SLEEP_1000 * (int) Math.pow(2, sourceNode.getHeight(false)));
		this.sendMessage(sourceNode, destination, message);
	}
	
	private void sendMessagePrompt(Scanner scan) throws InterruptedException 
	{
		DistributedTest.print("What node would you like to send a message from?");
		int source = scan.nextInt();
		Node sourceNode = db.get(source);
		if (sourceNode == null)
		{
			DistributedTest.print("Unable to find source node!");
			return;
		}
		DistributedTest.print("What node would you like to send a message to?");
		int destination = scan.nextInt();
		
		DistributedTest.print("What message would you like to send?");
		String message = scan.next();
		message += scan.nextLine();
		
		sendMessage(sourceNode, destination, message);

	}
	
	private void sendMessage(Node sourceNode, int destination, String message) throws InterruptedException
	{
		plugin.SendMessage visitor = new plugin.SendMessage();
		
		plugin.KeyValuePair<Integer, String> messagePair = new KeyValuePair<Integer, String>(destination, message);
		
		sourceNode.acceptWithResponse(visitor, messagePair);
		
		DistributedTest.print("\n");
	}

	private void runProgram()  throws InterruptedException
	{
		insertNode();
		insertNode();
		while (root.getHeight(false) < MAX_WEB_SIZE)
		{
			Thread.sleep(100);
			int index = randomIndex(9);
			DistributedTest.print("|PROGRAM| :: Running case: " + index);
			switch(index)
			{
				// insert a node
				case 0:
					insertNode();
					break;
				// insert many nodes
				case 1:
					insertNode();
					insertNode();
					break;
				// delete a node
				case 2:
					//deleteRandomNode();
					//break;
				// delete many nodes
				case 3:
					sendRandomMessage();
					//deleteRandomNode();
					//deleteRandomNode();

					break;
				// delete and insert
				case 4:
					randomBroadcast();
						//insertNode();
						//deleteRandomNode();
					insertNode();
						//deleteRandomNode();
						//insertNode();
					break;
				case 5:
				case 6:
					try 
					{
						Thread.sleep(SLEEP_1000);
					} 
					catch (InterruptedException e) 
					{
						e.printStackTrace();
					}
					break;
				// send random message
				case 7:
					sendRandomMessage();
					break;
				// broadcast random message
				case 8:
					randomBroadcast();
					break;
				default:
					break;
			}
			Thread.sleep(SLEEP_1000 / 2);
		}
		
	}
	
	private void insertNode() throws InterruptedException
	{
		DistributedTest.print("Inserting a node:");
		Node temp = new Node(PORT);
		db.put(temp.getGlobalID(), temp);
		root.insert(temp);
		DistributedTest.print("Node " + temp.getId(false) + " Inserted");
	}
	
	private void deleteRandomNode() throws InterruptedException
	{
		DistributedTest.print("Deleting randome node");
		ArrayList<Node> dbNodes = new ArrayList<Node>();
		for(Object value : db.getAllValues())
		{
			dbNodes.add((Node) value);
		}
		deleteNode(dbNodes.get(this.randomIndex(db.getAllKeys().size())).getId(false));
	}
	
	private void deleteNode() throws InterruptedException
	{
		Scanner scan = new Scanner(System.in);
		int input;
		DistributedTest.print("Enter node id of the node you want to delete");
		input = scan.nextInt();
		
		deleteNode(input);
	}
	
	private void deleteNode(int input) throws InterruptedException
	{
		try
		{
			if (db.getAllKeys().size() <= 1)
			{
				//DistributedTest.print("Unable to delete last node!\n");
				//return;
				
				DistributedTest.print("Deleting last node!!!");
			}
			Node obj = (Node) db.get(input);
			if (obj == null)
			{
				DistributedTest.print("Node not found!");
				return;
			}
			Phase1.GlobalID id = obj.getGlobalID();
			DistributedTest.print("Deleting node: " + input);
			DistributedTest.print("Object Removed: " + obj.remove());
			db.remove(id);
			
			if(root.getId(false) == obj.getId(false))
			{
				root = this.getNode();
				//DistributedTest.print("New Root: " + root.toString());
			}
			return;
			
		}
		catch (java.util.InputMismatchException e)
		{
			DistributedTest.print("Invalid input!");
			try 
			{
				Thread.sleep(0);
			} 
			catch (InterruptedException e1) 
			{
				e1.printStackTrace();
			}
		}
	}
	
	private int randomIndex(int max)
	{
		Random generator = new Random(System.currentTimeMillis() + BIG_RANDOM.nextInt());
		return generator.nextInt(max - 1 < 1 ? 1 : Math.abs(max - 1));
	}

	
	private Node getNode()
	{
		int bound = this.randomIndex(db.getAllKeys().size());
		int count = 0;
		for (Phase1.GlobalID key : db.getAllKeys())
		{
			// if you find a node in the database, insert the command node there
			if (this.db.get(key) instanceof Phase2.Node && count == bound)
			{
				return (Phase2.Node) this.db.get(key);
			}
			count++;
			
		}
		return null;
	}
	
	public static void connectToApp(Node root, Scanner scan)
	{
		print("Want to connect to another app?");
		String result = scan.next();
		if (result.contains("n") || result.contains("N"))
		{
			return;
		}
		print("Connecting to application on another machine...");
		print("Enter target ip address:");
		String ip = scan.next();
		print("Enter target port:");
		int port = getPort(scan);
		
		try 
		{
			Phase1.Client.sendData(ip, port, root);
			print("Node sent and connected");
		} 
		catch (UnknownHostException e) 
		{
			e.printStackTrace();
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
		} 
		catch (ClassNotFoundException e) 
		{
			e.printStackTrace();
		}
		
		
	}
	
	public static int getPort(Scanner scan)
	{
		Phase1.Port port = new Phase1.Port(3000);
		boolean isValidPort = false;
		do 
		{
			try
			{
				print("Enter the port number:");
				port = new Phase1.Port(scan.nextInt());
				isValidPort = true;
			}
			catch (NumberFormatException e)
			{
				print("Invalid!");
			}
		} while (!isValidPort);
		
		print("Port entered: " + port.getPort());
		return port.getPort();
	}
	
	public static void print(String args)
	{
		System.out.println(args);
	}
}
